/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.security;

import com.google.common.hash.HashCode;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.primitives.Ints.tryParse;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

/**
 * Encrypts password using <a href="https://en.wikipedia.org/wiki/PBKDF2">Password-based-Key-Derivative-Function</a>
 * with <b>SHA512</b> as pseudorandom function.
 * See <a href="https://www.ietf.org/rfc/rfc2898.txt">rfc2898</a>.
 *
 * @author Yevhenii Voevodin
 */
public class PBKDF2PasswordEncryptor implements PasswordEncryptor {

    private static final String  PWD_FMT   = "%s:%s:%d";
    private static final Pattern PWD_REGEX = Pattern.compile("(?<pwdHash>\\w+):(?<saltHash>\\w+):(?<iterations>[0-9]+)");

    private static final String       SECRET_KEY_FACTORY_NAME = "PBKDF2WithHmacSHA512";
    private static final SecureRandom SECURE_RANDOM           = new SecureRandom();
    /**
     * Minimum number of iterations required is 1_000(rfc2898),
     * pick greater as potentially safer in the case of brute-force attacks .
     */
    private static final int          ITERATIONS_COUNT        = 10_000;
    /** 64bit salt length based on the rfc2898 spec . */
    private static final int          SALT_LENGTH             = 64 / 8;

    @Override
    public String encrypt(String password) {
        requireNonNull(password, "Required non-null password");
        final byte[] salt = new byte[SALT_LENGTH];
        SECURE_RANDOM.nextBytes(salt);
        final HashCode hash = computeHash(password.toCharArray(), salt, ITERATIONS_COUNT);
        final HashCode saltHash = HashCode.fromBytes(salt);
        return format(PWD_FMT, hash, saltHash, ITERATIONS_COUNT);
    }

    @Override
    public boolean test(String password, String encryptedPassword) {
        requireNonNull(password, "Required non-null password");
        requireNonNull(password, "Required non-null encrypted password");
        final Matcher matcher = PWD_REGEX.matcher(encryptedPassword);
        if (!matcher.matches()) {
            return false;
        }
        // retrieve salt, password hash and iterations count from hash
        final Integer iterations = tryParse(matcher.group("iterations"));
        final String salt = matcher.group("saltHash");
        final String pwdHash = matcher.group("pwdHash");
        // compute password's hash and test whether it matches to given hash
        final HashCode hash = computeHash(password.toCharArray(), HashCode.fromString(salt).asBytes(), iterations);
        return hash.toString().equals(pwdHash);
    }

    private HashCode computeHash(char[] password, byte[] salt, int iterations) {
        try {
            final SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(SECRET_KEY_FACTORY_NAME);
            final KeySpec keySpec = new PBEKeySpec(password, salt, iterations, 512);
            return HashCode.fromBytes(keyFactory.generateSecret(keySpec).getEncoded());
        } catch (NoSuchAlgorithmException | InvalidKeySpecException x) {
            throw new RuntimeException(x.getMessage(), x);
        }
    }
}
